package grug.tian.minispringmvc;

import cn.hutool.core.exceptions.ExceptionUtil;
import cn.hutool.core.io.FileUtil;
import cn.hutool.core.io.file.FileReader;
import cn.hutool.core.util.StrUtil;
import cn.hutool.log.Log;
import cn.hutool.log.LogFactory;
import grug.tian.minispringmvc.annotation.Autowired;
import grug.tian.minispringmvc.annotation.Controller;
import grug.tian.minispringmvc.annotation.RequestMapping;
import grug.tian.minispringmvc.annotation.RequestParam;
import grug.tian.minispringmvc.annotation.Service;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.lang.annotation.Annotation;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.reflect.Parameter;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Properties;
import java.util.Set;
import javax.servlet.ServletConfig;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

/**
 * @author Grug.Tian
 * @date 2020/11/17 9:57
 */
public class DispatchServlet extends HttpServlet {

  private static final Log log = LogFactory.get();

  /**
   * 项目配置，类似SpringMVC的spring-mvc.xml配置
   * 当前只提供了包扫描路径的配置
   */
  private Properties contextConfig = new Properties();

  /**
   * 指定路所有的类名单
   */
  private Set<String> classNames = new HashSet<>();

  /**
   * IoC容器
   */
  private Map<String, Object> iocContainer = new HashMap<>();

  /**
   * URL --> handle method 映射
   */
  private Map<String, Method> handlerMap = new HashMap<>();

  @Override
  protected void doPost(HttpServletRequest request, HttpServletResponse response)
      throws ServletException, IOException {
    doGet(request, response);
  }

  @Override
  protected void doGet(HttpServletRequest request, HttpServletResponse response)
      throws ServletException, IOException {
    try {
      doDispatch(request, response);
    } catch (Exception e) {
      response500(response, e);
    }
  }

  /**
   * 初始化阶段
   */
  @Override
  public void init(ServletConfig config) throws ServletException {
    // 1.加载配置文件
    loadContextConfig(config);

    // 2.扫描配置路径下所有的类
    String scanPackage = contextConfig.getProperty("scanPackage");
    scanClassNames(scanPackage);

    // 3.实例化符合条件的类，并放入IoC容器
    initIocContainer();

    // 4.完成IoC托管类的依赖注入
    dependencyInject();

    // 5.初始化HandlerMapping
    initHandlerMapping();
  }

  /**
   * 加载 mini-springmvc 的配置。
   * 配置文件只支持properties格式，配置文件路径由 web.xml 指定，如果未指定则默认取 mini-springmvc.properties 。
   *
   * @param config Servlet配置
   */
  private void loadContextConfig(ServletConfig config) throws ServletException {
    String contextConfigLocation = config.getInitParameter("contextConfigLocation");
    if (StrUtil.isBlank(contextConfigLocation)) {
      contextConfigLocation = "mini-springmvc.properties";
    }
    FileReader fileReader = new FileReader(contextConfigLocation);
    InputStream contextConfigInputStream = fileReader.getInputStream();
    try {
      contextConfig.load(contextConfigInputStream);
    } catch (IOException e) {
      log.error("加载 mini-springmvc 配置文件失败", e);
      throw new ServletException(e);
    }

    String contextConfigContent = fileReader.readString();
    log.info("加载配置文件[{}]成功\n{}", contextConfigLocation, contextConfigContent);
  }

  /**
   * 扫描指定路径下所有的类
   *
   * @param scanPackage 扫描路径，例如：grug.tian.minispringmvc
   */
  private void scanClassNames(String scanPackage) {
    String scanPath = StrUtil.replace(scanPackage, ".", "/");
    log.debug("扫描路径[{}]下的类", scanPath);
    File[] files = FileUtil.ls(scanPath);
    for (File file : files) {
      if (file.isFile()) {
        if (FileUtil.getName(file).endsWith(".class")) {
          // 只扫描.class文件
          String className = scanPackage + "." + FileUtil.getName(file);
          className = StrUtil.replace(className, ".class", "");
          classNames.add(className);
        }
      } else {
        // 遇到文件夹就递归扫描
        scanClassNames(scanPackage + "." + FileUtil.getName(file));
      }
    }
  }

  /**
   * 初始化IoC容器，只有特定注解的类才需要托管到IoC容器
   */
  private void initIocContainer() {
    for (String className : classNames) {
      try {
        Class<?> clazz = Class.forName(className);
        if (clazz.isAnnotationPresent(Controller.class) || clazz.isAnnotationPresent(Service.class)) {
          Object bean = clazz.newInstance();
          String beanName = getBeanNameFromAnnotation(clazz);
          if (StrUtil.isBlank(beanName)) {
            beanName = buildBeanName(className);
          }
          iocContainer.put(beanName, bean);
          log.debug("类[{}]已经托管到IoC容器", className);
        }
      } catch (Exception e) {
        log.warn("初始化IoC容器出错，忽略当前错误继续执行", e);
      }
    }
  }

  /**
   * 返回 @Controller 或者 @Service 注解的value()，后续用作bean name
   */
  private String getBeanNameFromAnnotation(Class<?> clazz) {
    if (clazz.isAnnotationPresent(Controller.class)) {
      Controller controller = clazz.getAnnotation(Controller.class);
      return controller.value();
    } else if (clazz.isAnnotationPresent(Service.class)){
      Service service = clazz.getAnnotation(Service.class);
      return service.value();
    } else {
      return "";
    }
  }

  /**
   * 根据完整类名生成bean name，例如：grug.tian.minispringmvc.service.HelloService --> helloService
   * @param className 完整类名
   * @return bean名称
   */
  private String buildBeanName(String className) throws ClassNotFoundException {
    String simpleName = Class.forName(className).getSimpleName();
    return StrUtil.lowerFirst(simpleName);
  }

  /**
   * 给IoC容器类的被 @Autowired 修饰的字段注入值
   */
  private void dependencyInject() throws ServletException {
    for (Map.Entry<String, Object> entry : iocContainer.entrySet()) {
      Object object = entry.getValue();
      Class<?> clazz = object.getClass();
      // 获取所有字段，包括私有字段
      Field[] fields = clazz.getDeclaredFields();
      for (Field field : fields) {
        if (field.isAnnotationPresent(Autowired.class)) {
          // 该字段需要被注入
          try {
            Autowired autowired = field.getAnnotation(Autowired.class);
            String fieldObjectKey = autowired.value().trim();
            if (StrUtil.isBlank(fieldObjectKey)) {
              fieldObjectKey = buildBeanName(field.getType().getName());
            }
            field.setAccessible(true);
            field.set(object, iocContainer.get(fieldObjectKey));
            log.debug("类[{}]的字段[{}]被注入值", clazz.getName(), field.getName());
          } catch (Exception e) {
            log.error("依赖注入过程出错", e);
            throw new ServletException(e);
          }
        }
      }
    }
  }

  /**
   * 初始化HandlerMapping，URL --> handle method
   */
  private void initHandlerMapping() {
    for (Map.Entry<String, Object> entry : iocContainer.entrySet()) {
      Class<?> clazz = entry.getValue().getClass();
      if (!clazz.isAnnotationPresent(Controller.class)) {
        continue;
      }
      // 只获取public方法
      Method[] methods = clazz.getMethods();
      for (Method method : methods) {
        if (!method.isAnnotationPresent(RequestMapping.class)) {
          continue;
        }
        RequestMapping requestMapping = method.getAnnotation(RequestMapping.class);
        String url = requestMapping.value();
        handlerMap.put(url, method);
        log.debug("URL映射处理方法: [{}] --> [{}]", url, clazz.getName() + "#" + method.getName());
      }
    }
  }

  /**
   * 根据URL地址，将请求交给对应的Handler方法处理，并将处理后的结果输出到HttpServletResponse
   */
  private void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {
    String url = getRequestMappingUrl(request);
    if (!handlerMap.containsKey(url)) {
      response404(response);
      return;
    }

    Method method = handlerMap.get(url);
    Object controllerInstance = getControllerInstance(method);
    Object[] methodParams = getMethodParamValues(method, request, response);
    Object result = method.invoke(controllerInstance, methodParams);
    response.getWriter().write(result.toString());
  }

  /**
   * 从HttpServletRequest中获取RequestMapping格式的URL
   */
  private String getRequestMappingUrl(HttpServletRequest request) {
    String url = request.getRequestURI();
    String contextPath = request.getContextPath();
    url = url.replace(contextPath, "");
    return url;
  }

  private void response404(HttpServletResponse response) throws IOException {
    response.getWriter().write("<h1>404 Not Found</h1>");
  }

  private void response500(HttpServletResponse response, Exception e) throws IOException {
    String responseContent = "<h1>500 Server Error</h1>" + ExceptionUtil.stacktraceToString(e);
    response.getWriter().write(responseContent);
    log.warn(responseContent);
  }

  /**
   * 从IoC容器中获取Controller类的实例
   *
   * @param method Controller类的某个方法
   * @return Controller 对象实例
   */
  private Object getControllerInstance(Method method) throws Exception {
    String beanName = method.getDeclaringClass().getAnnotation(Controller.class).value();
    if (StrUtil.isBlank(beanName)) {
      beanName = buildBeanName(method.getDeclaringClass().getName());
    }
    return iocContainer.get(beanName);
  }

  /**
   * 从HttpServletRequest中传递的参数确认handler method各个参数的值
   * 支持使用 @RequestParam 注解规定的参数名，如果缺省注解，则使用参数名本身
   */
  private Object[] getMethodParamValues(Method method, HttpServletRequest request, HttpServletResponse response) {
    int parameterCount = method.getParameterCount();
    Class<?>[] parameterTypes = method.getParameterTypes();
    Parameter[] parameters = method.getParameters();
    Object[] methodParamValues = new Object[parameterCount];

    for (int paramIndex = 0; paramIndex < parameterCount; paramIndex++) {
      Class<?> parameterType = parameterTypes[paramIndex];
      if (parameterType == HttpServletRequest.class) {
        methodParamValues[paramIndex] = request;
      } else if (parameterType == HttpServletResponse.class) {
        methodParamValues[paramIndex] = response;
      } else {
        String paramName = "";
        Annotation[] annotations = parameters[paramIndex].getAnnotations();
        for (Annotation annotation : annotations) {
          if (annotation instanceof RequestParam) {
            paramName = ((RequestParam) annotation).value();
            break;
          }
        }
        if (StrUtil.isBlank(paramName)) {
          // 没有被 @RequestParam 注解修饰，就用自身名字作为参数名
          paramName = parameters[paramIndex].getName();
        }
        Object paramValue = request.getParameter(paramName);
        methodParamValues[paramIndex] = paramValue;
      }
    }
    return methodParamValues;
  }

}
